import datetime
import enum
import logging
import os
import signal
import traceback

from absl import flags, app
import hjson
import pandas as pd

import coin.proto.account_control_pb2 as control_proto
from coin.tool.strat_monitor.app.check_balance import (
    calculate_extra_balance)
from coin.proto.coin_query_pb2 import AccountBalance
from coin.proto.coin_request_pb2 import AccountRequestProto
from coin.support.accounting.app.check_fee_coin import get_all_application_detail
import coin.support.accounting.logic.account_status_util as account_status
from coin.support.account_control.approach_target_position_strategy import \
    ApproachTargetPositionStrategy
from xunkemgmt.support.accounting.coin.database.composite_querier import \
    query_exchange_rate
from xunkemgmt.support.accounting.logic.query_request import (
    get_all_requests,
    filter_request,
    select_account_request,
)
from xunkemgmt_client.tool.slack_noti import send_to_slack

FLAGS = flags.FLAGS


class BalanceConversionStatus(enum.Enum):
  PENDING = 1
  RUNNING = 2
  CANCELLED = 3
  COMPLETED = 4
  FAILED = 5


def collect_tickers(memcached_config_file):
  request_list = get_all_requests()
  request_list = select_account_request(request_list, account_group='Xunke')
  ticker_request_list = filter_request(request_list,
    filter_func=lambda x: (x.market_type, x.exchange, x.api_version))
  memcached_config = db_util.read_db_config(memcached_config_file)
  querier = MemcachedMarketInfoQuerier(db_config=memcached_config)
  ticker_response_list = querier.query_ticker_history(ticker_request_list)
  return ticker_response_list


class BalanceConversionController(object):
  def __init__(self, strategy_name, control_request, key_file, dry_run=False):
    assert isinstance(control_request, control_proto.ControlRequest)
    self._strategy_name = strategy_name
    self._control_request = control_request
    self._key_file = key_file
    self._strategy = ApproachTargetPositionStrategy(control_request, dry_run=dry_run)
    self._set_status(BalanceConversionStatus.PENDING)

  @property
  def strategy_name(self):
    return self._strategy_name

  @property
  def control_request(self):
    return self._control_request

  @property
  def status(self):
    return self._status

  def _set_status(self, status):
    assert isinstance(status, BalanceConversionStatus)
    self._status = status

  def run(self):
    signal.signal(signal.SIGTERM, self._strategy.signal_handler)
    signal.signal(signal.SIGINT, self._strategy.signal_handler)
    self._set_status(BalanceConversionStatus.RUNNING)
    try:
      self._strategy.run_live(strategy=self._strategy,
                              on_order_gateway_reset_callback=self._strategy.on_og_reset,
                              og_config_name=self._key_file,
                              init_order_gateway_after=datetime.timedelta(minutes=0.1))
      if self._strategy.strat_cancelled:
        self._set_status(BalanceConversionStatus.CANCELLED)
      else:
        self._set_status(BalanceConversionStatus.COMPLETED)
    except Exception:
      self._set_status(BalanceConversionStatus.FAILED)
      print(traceback.format_exc())


def _gen_controller_list(balance_checker_summaries,
                         balance_dict,
                         key_root,
                         *,
                         threshold_in_usd,
                         convert_currency,
                         lot_size,
                         tolerance,
                         edge_bp,
                         dry_run):
  currency_list = [convert_currency]
  for summary in balance_checker_summaries:
    currency_list.extend(summary['extra_balance'].keys())
  price_list = query_exchange_rate(currency_list, 'USD')
  price_dict = dict(zip(currency_list, price_list))

  target_set = set()
  controller_list = []
  for summary in balance_checker_summaries:
    market_type = summary['market_type']
    exchange = summary['exchange']
    owner = summary['owner']
    balance_key = (market_type, exchange, owner)
    assert market_type == 'Spot'
    account_balance = balance_dict[balance_key].balance
    account_request = AccountRequestProto(market_type=market_type, exchange=exchange, owner=owner)
    for quote, extra_balance in summary['extra_balance'].items():
      target_key = (balance_key, quote)
      key_file = _get_trade_key_file(key_root, market_type, exchange, owner)
      assert target_key not in target_set, target_key
      quote_in_usd = price_dict[quote]
      convert_currency_in_usd = price_dict[convert_currency]
      extra_balance_in_usd = extra_balance * quote_in_usd - threshold_in_usd
      convert_qty = round(extra_balance_in_usd / convert_currency_in_usd, 1)
      if convert_qty <= 0:
        continue
      init_balance = _get_balance(convert_currency, account_balance)
      target_qty = init_balance + convert_qty
      symbol = '%s-%s' % (convert_currency, quote)
      control_proto = _gen_control_proto(account_request=account_request,
                                         symbol=symbol,
                                         target_qty=target_qty,
                                         lot_size=lot_size,
                                         tolerance=tolerance,
                                         edge_bp=edge_bp)
      controller = BalanceConversionController(summary['strat_name'],
                                               control_proto,
                                               key_file,
                                               dry_run)
      controller_list.append(controller)
  return controller_list


def _gen_control_proto(*, account_request, symbol, target_qty, lot_size, tolerance, edge_bp):
  approach_position = control_proto.ApproachPositionRequest(target_position=target_qty,
                                                            pass_bp=edge_bp,
                                                            lot_size=lot_size,
                                                            target_error_tolerance=tolerance,
                                                            stickiness=0,
                                                            order_update_period=2)
  control_request = control_proto.ControlRequest(target=account_request,
                                                 symbol=symbol,
                                                 type=control_proto.CONTROL_APPROACH_POSITION,
                                                 approach_position=approach_position)
  return control_request


def _gen_balance_request(application_detail):
  request_list = get_all_requests(business_unit='Coin')
  market_type = ','.join(application_detail['market_type'].tolist())
  exchange = ','.join(application_detail['exchange'].tolist())
  owner = ','.join(application_detail['owner'].tolist())
  request_list = select_account_request(request_list,
                                        market_type=market_type,
                                        owner=owner,
                                        exchange=exchange)
  request_list = filter_request(request_list, filter_func=lambda x: (x.market_type, x.exchange, x.owner))
  return request_list


def _collect_balance(request_list):
  response_list = account_status.query_balance_list(request_list)
  assert len(request_list) == len(response_list)
  balance_dict = {}
  for balance_request, balance_response in zip(request_list, response_list):
    balance_key = (balance_request.market_type, balance_request.exchange, balance_request.owner)
    balance_dict[balance_key] = balance_response
  return balance_dict


def _get_balance(currency, account_balance):
  assert isinstance(account_balance, AccountBalance)
  balance = 0.
  for each_balance in account_balance.each_balance:
    if currency == each_balance.currency:
      balance = each_balance.total
      break
  return balance


def _get_trade_key_file(key_root, market_type, exchange, owner):
  dir_map = {
      ('Spot', 'Bithumb'): 'bithumb',
      ('Spot', 'Coinone'): 'coinone',
      ('Spot', 'Korbit'): 'korbit',
      ('Spot', 'Gopax'): 'gopax',
      ('Spot', 'Huobi'): 'Huobi',
      ('Spot', 'Okex'): 'OKEx',
      ('Spot', 'Quoinex'): 'quoinex',
      ('Spot', 'Upbit'): 'upbit',
  }
  exchange_dir = dir_map[(market_type, exchange)]
  key_dir = '%s/%s/%s' % (key_root, owner, exchange_dir)
  key_files = os.listdir(key_dir)
  assert len(key_files) == 1, key_dir
  key_file = os.path.join(key_dir, key_files[0])
  return key_file


def _get_result(controller_list, init_balance_dict, curr_balance_dict):
  columns = [
      'strategy',
      'market_type',
      'exchange',
      'owner',
      'symbol',
      'target',
      'init_base',
      'init_quote',
      'init_ts',
      'curr_base',
      'curr_quote',
      'curr_ts',
      'diff_base',
      'diff_quote',
      'status'
  ]
  results = []
  for controller in controller_list:
    result = {}
    control_request = controller.control_request
    market_type = control_request.target.market_type
    exchange = control_request.target.exchange
    owner = control_request.target.owner
    symbol = control_request.symbol
    balance_key = (market_type, exchange, owner)
    result['status'] = controller.status.name
    result['strategy'] = controller.strategy_name
    result['market_type'] = market_type
    result['exchange'] = exchange
    result['owner'] = owner
    result['symbol'] = symbol
    result['target'] = control_request.approach_position.target_position
    base, quote = symbol.split('-')
    result['init_base'] = _get_balance(base, init_balance_dict[balance_key].balance)
    result['init_quote'] = _get_balance(quote, init_balance_dict[balance_key].balance)
    result['init_ts'] = datetime.datetime.utcfromtimestamp(
        init_balance_dict[balance_key].query_timestamp)
    result['curr_base'] = _get_balance(base, curr_balance_dict[balance_key].balance)
    result['curr_quote'] = _get_balance(quote, curr_balance_dict[balance_key].balance)
    result['curr_ts'] = datetime.datetime.utcfromtimestamp(
        curr_balance_dict[balance_key].query_timestamp)
    result['diff_base'] = result['curr_base'] - result['init_base']
    result['diff_quote'] = result['curr_quote'] - result['init_quote']
    results.append(result)
  df = pd.DataFrame(results, columns=columns)
  return df


def _dump_result(controller_list, init_balance_dict, curr_balance_dict, slack_receiver=None):
  df = _get_result(controller_list, init_balance_dict, curr_balance_dict)
  result = df.to_string()
  print(result)
  if slack_receiver is not None:
    send_to_slack(result, slack_receiver, 'file')


def _start_controller(balance_request_list, controller_list, init_balance_dict, slack_receiver):
  for controller in controller_list:
    try:
      controller.run()
      curr_balance_dict = _collect_balance(balance_request_list)
      _dump_result(controller_list, init_balance_dict, curr_balance_dict, slack_receiver)
    except Exception:
      print(traceback.format_exc())
    finally:
      if controller.status == BalanceConversionStatus.CANCELLED:
        break


def main(argv):
  with open(FLAGS.strat_file) as f:
    strat_list = hjson.loads(f.read())
  application_detail = get_all_application_detail().set_index('strategy_name')
  application_detail = application_detail.loc[strat_list]

  ticker_list = collect_tickers(FLAGS.memcached_config)
  balance_request_list = _gen_balance_request(application_detail)
  init_balance_dict = _collect_balance(balance_request_list)

  balance_checker_summary = calculate_extra_balance(strat_list,
                                                    ticker_list,
                                                    FLAGS.kafka_config_filename)
  controller_list = _gen_controller_list(balance_checker_summary,
                                         init_balance_dict,
                                         FLAGS.trade_key_root,
                                         threshold_in_usd=FLAGS.threshold_in_usd,
                                         convert_currency=FLAGS.currency,
                                         lot_size=FLAGS.lot_size,
                                         tolerance=FLAGS.tolerance,
                                         edge_bp=FLAGS.edge_bp,
                                         dry_run=FLAGS.dry_run)
  slack_receiver = FLAGS.slack_receiver
  _dump_result(controller_list, init_balance_dict, init_balance_dict, slack_receiver)
  _start_controller(balance_request_list, controller_list, init_balance_dict, slack_receiver)


if __name__ == '__main__':
  logging.basicConfig(level='DEBUG', format='%(levelname)s %(asctime)s %(name)s] %(message)s')

  flags.DEFINE_string('strat_file', None, 'strat file')
  flags.DEFINE_string('kafka_config_filename',
                      '../../coin_deploy/support_monitor/config/kafka_config.json',
                      'kafka config')
  flags.DEFINE_string('memcached_config',
                      "../../coin_deploy/support_accounting/db_config/memcached_config_prod.json",
                      'path of database config.')
  flags.DEFINE_string('trade_key_root', '../../coin_key_high/trade', 'path of database config.')
  flags.DEFINE_string('currency', 'BTC', 'target conversion currency')
  flags.DEFINE_float('lot_size', 0.1, 'lot size')
  flags.DEFINE_float('tolerance', 0.1, 'target_error_tolerance')
  flags.DEFINE_float('edge_bp', 2, 'edge_bp')
  flags.DEFINE_float('threshold_in_usd', 5000, 'threshold in usd')
  flags.DEFINE_boolean('dry_run', False, 'dry_run')
  flags.DEFINE_string('slack_receiver', None, 'slack recevier')

  app.run(main)
